<?php
namespace BLL\DataModel;

use ArrayAccess;
use Exception;

/**
 * DataNodeDescription
 *
 * schema of a DataNode
 *
 * @author David Wu
 */
class DataNodeDescription extends DataDescription implements ArrayAccess   {
    
    /**
     * Is this node contains multi rows.
     * true: multi lines
     * false: single line 
     *
     * determinded by master node
     *
     * @var bool
     */    
    public $isMulti = false;
        
    /**
     * min rows must be contained in this node.
     *
     * determinded by master node
     *
     * @var int
     */    
    public $minCount = 0;

    /**
     * max rows must be contained in this node.
     *
     * determinded by master node
     *
     * @var int
     */    
    public $maxCount = 0;

    /**
     * this table of this node.
     * 
     * @var string
     */    
    protected $realTableName = null;
    
    /**
     * this node is a part of another node, the master node.
     *
     * can be determinded by master node
     *
     * @var DataNodeDescription
     */    
    public $masterNode = null;

    /**
     * slave nodes contained.
     * 
     * @var Array of DataNodeDescription
     */    
    public $nodes = array();

    /**
     * itemes of the node.
     * 
     * @var Array of DataItemDescription
     */    
    public $items = array();

    /**
     * config file of this node.
     * 
     * if this filed is null, this node is described by its master node.
     * 
     * @var string
     */    
    public $configFile = null;
    
    /**
     * when this node contains salve nodes, this filed contains a short name to build tablename of slave nodes.
     *
     *
     * determinded by master node
     *
     * @var string
     */    
    public $nameInPrefix=null;
    
    /**
     * MasterID field Name
     *
     * determinded by master node
     *
     * @var string
     */    
    public $masterIDField = "master_id";
    
    /**
     * ID field Name
     *
     * @var string
     */    
    public $idField = "id";


    /**
     * ID Point To Top Level Master Node
     * true: can not ne null
     * false: can be null
     *
     * determinded by master node
     * 
     * @var string
     */
    protected $rootIDField = null;
    
    public $fromDB = true;
    
    public function getRootIDField(){
        if($this->rootIDField)return $this->rootIDField;
        if(!$this->masterNode)return $this->idField;
        return $this->masterNode->getRootIDField();
    }
    
    /**
     * Construct method

     * @param string $name "config file nmae"
     * 
     */
    function __construct($name=null){
        if($name){
            return $this->loadFromConfig($name);
        }
    }
    
    /**
     * Get descendant node from master nodes by name trail array
     * 
     * @param string_array $names "this names begin with top-level master node, till the master node of this node."
     * 
     * @throws Exception "Unexpected Data Node Name"
     * 
     * @return DataNodeDescription
     */
    public static function getNodeByNameTrail($nameTrail,$force = false){
        if (!$nameTrail) {
            return null;
        }
        if (is_string($nameTrail)) {
            $nameTrail = array($nameTrail);
        }
        if (is_array($nameTrail) && count($nameTrail) > 0) {
            
            $node = self::getNodeFromCacheOrConfigByName($nameTrail[0], $force);
            $tempNameTrace = $nameTrail[0];
            for ($i = 1; $i < count($nameTrail); $i++) {
                $tempNameTrace = $tempNameTrace . "\\$nameTrail[$i]";
                if (!($node = $node[$nameTrail[$i]])) {
                    throw new Exception("Unexpected Data Node Name:" . $tempNameTrace);
                }
            }
            return $node;
        }
        return null;
    }
    
    public static function getNodeByFullName($fullName,$force = false){
        if (starts_with($fullName, self::ModelClassBase . "\\")) {
            $fullName = substr($fullName, strlen(self::ModelClassBase)+1);
        }
        return Self::getNodeByNameTrail(explode("\\",$fullName) , $force);
    }
    /**
     * Get node by name
     * 
     * If already in cache, get it
     * If not in cache, read from config file, and put it in cache
     * Cache enabled
     * 
     * @param string $name "node name"
     * 
     * @return DataNodeDescription
     */
    private static function getNodeFromCacheOrConfigByName($name,$force = false){
        $tempCacheName = "DataSchema:{$name}";
        $node = null;
        if (!$force && \Cache::has($tempCacheName)) {
            $node = \Cache::get($tempCacheName);
        } else {
            $node = new DataNodeDescription($name);
            \Cache::put($tempCacheName, $node,10000);
        }
        return $node;
    }
    
    /**
     * Get slave node by name 
     * 
     * this names begin with top-level master node, till the master node of this node.
     * 
     * @names array of string
     * 
     * @throws Exception if this node not exist
     * 
     * @return Descriptionreturn
     */
    public function getSlaveNodeByName($name) {
        if ($this[$name]) {
            return $this[$name];
        } else {

            $node = $this;
            $tempNameTrace = $name;
            while ($node) {
                $tempNameTrace = $node->name . "\\{$tempNameTrace}";
                $node = $node->masterNode;
            }
            throw new Exception("Unexpected Data Node Name:" . $tempNameTrace);
        }
    }

    /**
     * Load node schema from configFile 
     * 
     * @configName string
     * 
     * @throws Exception if this node not exist
     * 
     * @return bool  
     *  true if the file exist 
     *  false if the file is not exist
     */
    private function loadFromConfig($configName=null){
        
        if(empty($configName)){
            $configName=$this->name;
        }
        $path = str_replace("\\", DIRECTORY_SEPARATOR, $GLOBALS["AppConfig"]["dataStruct"]);
        $path = str_replace("/", DIRECTORY_SEPARATOR, $path);
        if (!strrchr($path, DIRECTORY_SEPARATOR)) {
            $path = $path . DIRECTORY_SEPARATOR;
        }
        $filename=  app_path().DIRECTORY_SEPARATOR.$path.$configName.".json";
        if(file_exists($filename)){
            $this->configFile=$configName;
            $str = file_get_contents($filename) ;
            $_jsonObject = json_decode($str);
            $this->loadFromJSonObject($_jsonObject);
            return true;
        }
        return false;
    }
    
/*
 *
 * this part should be move into data contract 
 *  
 */    
    /**
     * Data Scription Driver fit this node.
     * true: can not ne null
     * false: can be null
     *
     * @var bool
     */
    public $driver = null;
    public $jsonBase = "";
    public $jsonAttributesToArray = false;
    public $jsonAttributesToArrayOmit = null;
    public $jsoneOmitRowCondition=null;
    /**
     * Load node schema from json object 
     * 
     * @param JSON $_jsonObject
     * 
     * @throws Exception when top Level Node reefer To another config file
     */
    protected function loadFromJSonObject($_jsonObject){
        
        parent::loadFromJSonObject($_jsonObject);

        if(isset($_jsonObject->configFile)){
            $this->loadFromConfig($_jsonObject->configFile);
        }else{
            $this->loadItemsFromJSONObject($_jsonObject);
            $this->loadNodesFromJSONObject($_jsonObject);
        }
        
        parent::loadFromJSonObject($_jsonObject);
        $this->readAttrFromJSon( $_jsonObject, "idField", DataType::String );
        $this->readAttrFromJSon( $_jsonObject, "masterIDField", DataType::String );
        $this->readAttrFromJSon( $_jsonObject, "realTableName", DataType::String );
        $this->readAttrFromJSon( $_jsonObject, "nameInPrefix",  DataType::String );
        $this->readAttrFromJSon( $_jsonObject, "isMulti",  DataType::Bool);
        $this->readAttrFromJSon( $_jsonObject, "minCount", DataType::Int);
        $this->readAttrFromJSon( $_jsonObject, "maxCount", DataType::Int);
        $this->readAttrFromJSon( $_jsonObject, "rootIDField", DataType::String);
        $this->readAttrFromJSon( $_jsonObject, "fromDB", DataType::Bool);

        
        $this->readAttrFromJSon( $_jsonObject, "driver", DataType::String);
        $this->readAttrFromJSon( $_jsonObject, "jsonBase", DataType::String);
        $this->readAttrFromJSon( $_jsonObject, "jsonAttributesToArray", DataType::Bool);
        $this->readAttrFromJSon( $_jsonObject, "jsonAttributesToArrayOmit", DataType::String);
       
        if(isset($_jsonObject->dateSourceType)){
            $this->dataType = DataSourceType::fromString($_jsonObject->dateSourceType);
        }
       
    }

    /**
     * Load items schema from json object 
     * 
     * @param JSON _jsonObject
     * 
     */
    private function loadItemsFromJSONObject($_jsonObject){
        if(isset($_jsonObject->items)){
            foreach($_jsonObject->items as $item){
                $newItem = new DataItemDescription($item);
                $this[$newItem->name]=$newItem;
            }
        }
    }

    /**
     * Load slave nodes schema from json object 
     * 
     * @param JSON _jsonObject
     * 
     */
    private function loadNodesFromJSONObject($_jsonObject){
        if(isset($_jsonObject->nodes)){
            foreach($_jsonObject->nodes as $item){
                $newItem = new DataNodeDescription();
                $newItem->loadFromJSonObject($item);
                $newItem->masterNode = $this;
                $this[$newItem->name]=$newItem;
            }
        }
    }
    
    public function offsetExists($offset) {
        return isset($this->nodes[$offset]) || isset($this->items[$offset]);
    }

    public function offsetGet($offset) {
        if (isset($this->items[$offset])) {
            return $this->items[$offset];
        }
        if (isset($this->nodes[$offset])) {
            return $this->nodes[$offset];
        }
        return null;
    }

    public function offsetSet($offset, $value) {
        
        if($value === null){
            throw new Exception('Null Parameter.');
        }
        if($value instanceof DataDescription){
            $offset = $value->name; 
        }else{
            throw new Exception('Wrong Data Type: expect DataDesction' . $value);
        }
        if($value instanceof DataItemDescription){
            if(isset($this->nodes[$offset])){
                throw new Exception('Item with the same name already exist in slave nodes: ' . $offset );
            }
            $this->items[$offset] = $value;
        }
        if($value instanceof DataNodeDescription){
            if(isset($this->items[$offset])){
                throw new Exception('Item with the same name already exist in items: ' . $offset );
            }
            $this->nodes[$offset] = $value;
        }
    }
    public function offsetUnset($offset) {
        if (isset($this->items[$offset])) {
            unset($this->items[$offset]);
        }elseif (isset($this->nodes[$offset])) {
            unset($this->nodes[$offset]);
        }
    }
    
    /**
     * Get Table Name of this node 
     * 
     * @return string
     */
    public function getRealTableName() {
        if ($this->realTableName && $this->realTableName<>"") {
            return $this->realTableName;
        }
        $realName = $this->name;
        $tempNode = $this;
        while ( !starts_with($realName, "/") && $tempNode->masterNode ) {
            $realName = $tempNode->masterNode->nameInPrefix . "_" . $realName;
            $tempNode = $tempNode->masterNode;
        }
        if(starts_with($realName, "/")){
            $realName = substr($realName, 1);
        }
        return $realName;
    }
   
    const ModelClassBase = "AppModel";
    
    public function getNameSpace() {
        $tempNode = $this->masterNode;
        if(!$tempNode)return self::ModelClassBase;
        $nameSpace = $tempNode->name;
        while ( $tempNode->masterNode ) {
            $tempNode = $tempNode->masterNode;
            $nameSpace = $tempNode->name . "\\" . $nameSpace;
        }
        return self::ModelClassBase ."\\" . $nameSpace;
    }
    
    public function getFullName() {
        return $this->getNameSpace() . "\\" . $this->name;
    }
}

